<?php
/**
 * snowflake雪花算法生成器
 * @author: langming
 * @date: 2021/8/26 下午2:05
 */

namespace Langming\Snowflake;

use Closure;
use Swoole;
use Laravel\Octane\Facades\Octane;

/**
 * 生成结构
 * 不支持32位系统
 * 0--秒级时间戳--序列号--业务id--机器id
 *
 * datacenter这里建议用作业务id，是通过配置文件传入的
 * worker是服务启动时候自动注册的，读取网卡信息注册到db，获取只是启动时一次执行，不会影响之后的生成效率
 * 秒级时间戳可以解决服务器时间回跳问题（服务器在时间同步下毫秒级快慢误差很大）
 * 秒级以内的用序列号区分，采用swoole table和进程锁实现单台worker下的id自增
 *
 * 位运算&：按位与，将把 $a 和 $b 中都为 1的位设为 1
 * 位运算|：按位或，将把 $a 和 $b 中任何一个为 1的位设为 1
 * 位运算^：按位异或，将把 $a 和 $b 中一个为 1另一个为 0 的位设为 1(异)
 * 位运算<<：左移
 * 位运算>>：右移
 */
final class Snowflake
{

    private int $epoch; //开始时间戳
    private int $dataCenterId; ////据标识id（业务id）
    private int $workerId; //机器id

    private int $sequenceBits; //序列号所占位数
    private int $dataCenterIdBits; //据标识id所占的位数（业务id）
    private int $workerIdBits; //机器id所占位数

    /**
     * 初始化配置信息（在容器注册时执行）
     * @param array $config
     * @return $this
     */
    public function config(array $config): self
    {
        $this->epoch = strtotime($config['epoch']);
        $this->dataCenterId = $config['data_center_id'];
        $this->workerId = $config['worker_id'];

        $this->sequenceBits = $config['sequence_bits'];
        $this->dataCenterIdBits = $config['data_center_id_bits'];
        $this->workerIdBits = $config['worker_id_bits'];
        return $this;
    }

    /**
     * 生成id
     * @param Closure|null $callback 如果生成异常执行的闭包（可用于监控）
     * @return string
     */
    public function create(?Closure $callback = null): string
    {
        //0--秒级时间戳--序列号--业务id--机器id
        $current = $this->getTime();
        $seqId = 0;
        $maxSeq = -1 ^ (-1 << $this->sequenceBits); //seq最大值

        //加进程锁
        $lock = new \Swoole\Lock(SWOOLE_MUTEX); //互斥锁
        $lock->lockwait(1);

        //读取上一次生成数据
        $table = $this->getTable();
        if (!$table) {
            $table = [
                'time' => 0,
                'seq' => 0
            ];
        }

        //$timestamp = $this->waitTime($table['time']);  //没有采用阻塞等待下一秒

        //如果当前小于上一次生成时间则出现了时间回拨
        //借用上一次时间，直至序列号id写满
        if ($current <= $table['time']) {
            if ($current < $table['time'] && $callback) {
                call_user_func_array($callback, [-1, "time delay " . $table['time'] - $current . "second"]);
            }
            $timestamp = $table['time'];
            $seqId = $table['seq'] + 1;
        } else {
            $timestamp = $current;
            $seqId = 1;
        }

        //如果seq超出范围，则借用下一秒的时间
        if ($seqId > $maxSeq) {
            $timestamp = $timestamp + 1; //借用下一秒时间
            $seqId = 1;
            if ($callback) {
                call_user_func_array($callback, [-2, "{$current} Seq has reached the maximum value"]);
            }
        }

        //写入本次生成数据到内存表
        $this->setTable([
            'time' => $timestamp,
            'seq' => $seqId
        ]);

        //释放锁
        $lock->unlock();

        //左移指定位数,目的是将位置用0空出
        $workerIdBin = $this->workerId;
        $dataCenterIdBin = $this->dataCenterId << $this->workerIdBits;
        $sequenceIdBin = $seqId << ($this->dataCenterIdBits + $this->workerIdBits);
        $timestampBin = $timestamp << ($this->sequenceBits + $this->dataCenterIdBits + $this->workerIdBits);

        //最后用|运算合并
        return $timestampBin | $sequenceIdBin | $dataCenterIdBin | $workerIdBin;
    }

    /**
     * 解析id
     * @param int $id
     * @return array
     */
    public function parse(int $id): array
    {
        $timeOffset = $this->sequenceBits + $this->dataCenterIdBits + $this->workerIdBits; //时间戳偏移位数
        $timestamp = $id >> $timeOffset;

        $seqOffset = $this->dataCenterIdBits + $this->workerIdBits; //序列号偏移位数

        //id右移将序列号的右侧抛弃， 时间戳左移序列号位数用0补充 再异或即为seq
        $seqId = ($id >> $seqOffset) ^ ($timestamp << $this->sequenceBits);

        //右移datacenter + worker位，再向左移datacenter位 此时datacenter全为0
        $dataCenterId = ($id >> $seqOffset) << $this->dataCenterIdBits;
        //再与移除worker位的数做异或即为datacenter
        $dataCenterId = $dataCenterId ^ ($id >> $this->workerIdBits);

        //右移再左移worker位数，worker将变为0，再和id本身亦或
        $workerId = (($id >> $this->workerIdBits) << $this->workerIdBits) ^ $id;

        return [
            'datetime' => date('Y-m-d H:i:s', $timestamp + $this->epoch),
            'timestamp' => $timestamp,
            'seq' => $seqId,
            'data_center_id' => $dataCenterId,
            'worker_id' => $workerId
        ];
    }

    /**
     * 获取当前时间戳
     * @return int
     */
    public function getTime(): int
    {
        return intval(time() - $this->epoch);
    }

    /**
     * 阻塞等待下一个时间戳
     * @param int $lastTimestamp
     * @return int
     */
    public function waitTime(int $lastTimestamp): int
    {
        $timestamp = $this->getTime();

        //当前时间小于上一个时间进入循环
        while ($timestamp < $lastTimestamp) {
            $timestamp = $this->getTime();
        }
        return $timestamp;
    }

    /**
     * 获取swoole table数据
     * @return array|bool
     */
    public function getTable(): array|bool
    {
        return Octane::table('snowflake')->get('id');
    }

    /**
     * 写入swoole table数据
     * @param array $table
     */
    public function setTable(array $table): void
    {
        Octane::table('snowflake')->set('id', $table);
    }

    private function __clone()
    {
    }
}
